/*
 Created by lustre on 2021/12/5.
 version 0.0.4.beta
*/

#ifndef LIGHT_TCP_H
#define LIGHT_TCP_H

#define LIGHT_TCP_VERSION "0.0.4"

// 引入当前头文件需要先引入Arduino.h

#ifdef ESP8266

#include <ESP8266WiFi.h>

#elif defined ESP32

#include <WiFi.h>
#include <MD5Builder.h>

#endif

#include <WiFiUdp.h>
//上位机为热点时 接收通知的端口
#define AS_GATE_WAY_PORT 8001

// cmd字段
#define CMD_ID "id"
#define OP_CODE "op"
#define MSG_OK 0
#define MSG_FAILED (-1)

typedef struct {
    int cmdIdVal;
    int opCode;
} MsgHeader;

typedef struct {
    // wifi 的ssid和密码
    char const *ssid;
    char const *passphrase;
    // 服务端地址与端口
    char const *host;
    uint16_t serverPort;
    // 设备用户名密码
    char const *userName;
    char const *passwd;
    // 设备属性数，和属性
    uint16_t attrCount;
    char const *devAttr;
} ConnectInfo;

class LightClient {
private:
    WiFiClient client;


#define TOPIC_LEN 12


    static IPAddress getServerHost() {
        WiFiUDP udp;
        uint8_t buf[TOPIC_LEN];
        // 加入组，接收来自上位机的组播
#ifdef ESP8266
        udp.beginMulticast(IPAddress(INADDR_ANY), IPAddress(224, 0, 0, 5), 8000);
#elif defined( ESP32)
        udp.beginMulticast(IPAddress(224, 0, 0, 5), 8000);
#endif
        Serial.println("get server ip...");
        const uint8_t TOPIC[12] = {'l', 'i', 'g', 'h', 't', '-',
                                   's', 'e', 'r', 'v', 'e', 'r'};

        // 当上位机开热点的时候 大概率组播路由不会走它自己的ap的网段，这里向热点ip网关发送信息，
        // 如果对方是上位机，收到当前包 ，就往当前地址发包 来确认身份
        uint8_t client_topic[12] = {'l', 'i', 'g', 'h', 't',
                                    '-', 'c', 'l', 'i', 'e', 'n', 't'};
        const IPAddress &gwIp = WiFi.gatewayIP();

        while (true) {
            int len = udp.parsePacket();
            if (len >= TOPIC_LEN) {
                // 先尝试接受组播，
                udp.read(buf, TOPIC_LEN);
                if (std::equal(std::begin(TOPIC), std::end(TOPIC), std::begin(buf))) {
                    const IPAddress &address = udp.remoteIP();
                    Serial.println();
                    Serial.print("remote ip is:");
                    Serial.println(address);
                    udp.stop();
                    return address;
                }

            }
            udp.beginPacket(gwIp, AS_GATE_WAY_PORT);
            udp.write(client_topic, 12);
            udp.endPacket();
            Serial.print("\r.");
            delay(400);
            Serial.print("\r..");
        }
    }

public:
    int readInt(char terminator) {
        String str = client.readStringUntil(terminator);
        return str.toInt();
    }

    String readKey() {
        String key = client.readStringUntil(':');
        key.trim();
        return key;
    }

    // 发现不是key 丢掉后续
    void dropLine(const String &key) {
        Serial.print("drop msg:\n\t");
        Serial.print(key);
        Serial.print(':');
        Serial.print(client.readStringUntil('\n'));
    }

    // 读字符串
    String readStringUntil(char terminator) {
        return client.readStringUntil(terminator);
    }

    int readMsgHeader(MsgHeader *msgHeader) {
        String cmdIdKey = readKey();
        if (cmdIdKey.isEmpty()) {
            return MSG_FAILED;
        }
        if (!cmdIdKey.equals(CMD_ID)) {
            dropLine(cmdIdKey);
            return MSG_FAILED;
        }
        int cmdIdVal = readInt('\n');

        String opCodeKey = readKey();
        if (!opCodeKey.equals(OP_CODE)) {
            dropLine(opCodeKey);
            return MSG_FAILED;
        }
        int opCode = readInt('\n');

        msgHeader->cmdIdVal = cmdIdVal;
        msgHeader->opCode = opCode;
        return MSG_OK;
    }

    static void connectWifi(const char *ssid, const char *passphrase) {
        WiFi.mode(WIFI_STA);
        WiFi.begin(ssid, passphrase);


        Serial.println("connecting to router... ");
        //等待wifi连接成功
        while (WiFi.status() != WL_CONNECTED) {
            Serial.print(".");
            delay(500);
        }
        Serial.print("\nWiFi connected, local IP address:");
        Serial.println(WiFi.localIP());
        Serial.print("Wifi GWip is:");
        Serial.println(WiFi.gatewayIP());
    }

    bool login(const char *userName,
               const char *passwd) {
        // 获取nonce
        uint8_t nonce[8];
        this->client.write("nonce");
        this->client.flush();
        Serial.println("send nonce request\nnone len:");
        size_t len;
        do {
            Serial.print("\rtry get none:");
            len = this->client.readBytes(nonce, 8);
            Serial.print(len);
        } while (len < 1);

        Serial.println("\nget nonce down");
        // 发送用户名
        this->client.write(strlen_P(userName));
        this->client.write(userName);
        this->client.flush();
        Serial.println("send userName");
        // md5(userName + passwd)
        uint8_t md[16];
        MD5Builder md5Builder{};
        md5Builder.begin();
        md5Builder.add(userName);
        md5Builder.add(passwd);
        md5Builder.calculate();
        md5Builder.getBytes(md);
        // 认证的摘要 md5(nonce + md5(userName + passwd))
        md5Builder.begin();
        md5Builder.add(nonce, 8);
        md5Builder.add(md, 16);
        md5Builder.calculate();
        md5Builder.getBytes(md);
        this->client.write(md, 16);
        this->client.flush();
        Serial.println("send message digest");
        delay(200);
        return this->client.connected();
    }


    void helloServer(const uint16_t attrCount, const char *devAttr) {
        //与服务端握手
        client.println("hello");
        uint8_t mac[6];
        WiFi.macAddress(mac);
        Serial.printf("MAC is:%02X%02X%02X%02X%02X%02X\n", mac[0], mac[1],
                      mac[2], mac[3], mac[4], mac[5]);
        // sn 长度，这里将mac作为设备序列号。长度为6字节
        client.write(6);
        client.write(mac, 6);
        client.write('\n');
        //属性行数，占两个字节
        client.write((0xff00 & attrCount) >> 8);
        client.write(0xff & attrCount);
        client.write('\n');
        client.write(devAttr);
        client.write('\n');
        client.flush();
    }

    IPAddress connectServer(ConnectInfo *connectInfo) {
        Serial.println("light client version:"
                       LIGHT_TCP_VERSION
        );
        connectWifi(connectInfo->ssid, connectInfo->passphrase);

        IPAddress address((uint32_t) 0);
        if (connectInfo->host == nullptr) {
            Serial.println("Server host is not specified");
            address = getServerHost();
        } else {
            Serial.println("Server host is specified as :");
            Serial.println(connectInfo->host);
            if (!(WiFi.hostByName(connectInfo->host, address))) {
                Serial.println("get host by name failed");
                return address;
            }
        }


        if (WiFi.status() != WL_CONNECTED) {
            return false;
        }
        while (!client.connect(address, connectInfo->serverPort)) {
            Serial.println("connection failed,wait 1 sec...");
            delay(1000);
        }
        Serial.println("connect to tcp server success.");
        this->client.setNoDelay(true);
        if (!login(connectInfo->userName, connectInfo->passwd)) {
            Serial.println("login in failed!");
            return false;
        }
        helloServer(connectInfo->attrCount, connectInfo->devAttr);

        return address;
    }


};

#endif  // LIGHT_TCP_H
